package com.zhang.zxx.boot.common.core.aspect;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.ApiOperation;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;

/**
 * WebLogAspect:web切面示例
 *
 * @author zhangxiaoxiang
 * @date 2021年11月27日
 */
@Slf4j
@Aspect
@Component
@Profile({"dev", "prod", "test"})
public class WebLogAspect {

    /**
     * 换行符f
     */
    private static final String LINE_SEPARATOR = System.lineSeparator();

    public static final ObjectMapper objectMapper;

    static {
        objectMapper = new ObjectMapper() {{
            setAnnotationIntrospector(new WebLogIgnoredPropertyAnnotationIntrospector());
        }};
        objectMapper.setLocale(Locale.CHINA);
        objectMapper.setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
        objectMapper.setDateFormat(new SimpleDateFormat(DatePattern.NORM_DATETIME_PATTERN));
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * 以自定义 @WebLog 注解为切点
     */
    @Pointcut(value = "@annotation(webLog)", argNames = "webLog")
    public void webLogPoint(WebLog webLog) {
    }

    /**
     * 环绕增强
     *
     * @param joinPoint joinPoint
     * @param webLog    webLog
     * @return Object
     */
    @Around(value = "webLogPoint(webLog)", argNames = "joinPoint,webLog")
    public Object doAround(ProceedingJoinPoint joinPoint, WebLog webLog) throws Throwable {

        // 开始打印请求日志 ApiOperation接口描述 RequestMapping接口路径
        ApiOperation apiOperation = null;
        RequestMapping requestMapping = null;
        try {
            String methodDescription;
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            Method currentMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
            Annotation[] methodAnnotations = currentMethod.getAnnotations();
            // 查找注解 接口文档的注解ApiOperation
            for (Annotation annotation : methodAnnotations) {
                apiOperation = annotation instanceof ApiOperation ? (ApiOperation) annotation : apiOperation;
                requestMapping = AnnotatedElementUtils.findMergedAnnotation(currentMethod, RequestMapping.class);
            }

            // 准备打印参数
            String logArgStr = this.getLogArgStr(webLog, joinPoint);

            // 判断是否为Controller层的方法
            if (Objects.nonNull(requestMapping)) {
                assert attributes != null;
                HttpServletRequest request = attributes.getRequest();
                // 填充操作信息
                if (Objects.nonNull(apiOperation)) {
                    // 绑定到RequestContextHolder中
                    attributes.setAttribute("apiOperation", apiOperation, RequestAttributes.SCOPE_REQUEST);
                    methodDescription = apiOperation.value();
                } else {
                    methodDescription = this.getAspectLogDescription(currentMethod, webLog);
                }
                //前置增强[都在环绕增强一并做了]
                this.printRequestBeforeLog(joinPoint, request, methodDescription, logArgStr, true);
            } else {
                methodDescription = this.getAspectLogDescription(currentMethod, webLog);
                this.printMethodBeforeLog(joinPoint, methodDescription, logArgStr, true);
            }
        } catch (Exception e) {
            log.warn("@Weblog打印日志失败");
        }

        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long endTime = System.currentTimeMillis();
        long consuming = endTime - startTime;

        try {
            String resultStr;
            if (result instanceof byte[]) {
                resultStr = "byte[" + ((byte[]) result).length + "]";
            } else {
                resultStr = objectMapper.writeValueAsString(result);
            }
            if (Objects.nonNull(requestMapping)) {
                this.printRequestAfterLog(consuming, resultStr, true);
            } else {
                this.printMethodAfterLog(consuming, resultStr, true);
            }
        } catch (Exception e) {
            log.warn("@Weblog打印日志失败");
        }
        return result;
    }

    /**
     * 获取参数
     *
     * @param webLog    weblog注解
     * @param joinPoint joinPoint
     * @return 参数
     */
    @SneakyThrows
    private String getLogArgStr(WebLog webLog, JoinPoint joinPoint) {
        String[] params = webLog.params();
        Object printArg = params.length > 0 ? this.scanParamsArg(joinPoint, params) : this.scanRequestBodyArg(joinPoint);
        return (null != printArg) ? objectMapper.writeValueAsString(printArg) : null;
    }

    /**
     * <h3>扫描切点上与paramNames匹配的参数</h3>
     *
     * @param joinPoint  切点
     * @param paramNames 参数名称
     * @return arg/null
     */
    private Object scanParamsArg(JoinPoint joinPoint, String[] paramNames) {
        //TODO 找到参数就是用参数
        return null;
    }

    /**
     * <h3>扫描切点上带RequestBody注解的参数</h3>
     *
     * @param joinPoint 切点
     * @return arg/null
     */
    @Nullable
    private Object scanRequestBodyArg(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();
        Method currentMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Annotation[][] parameterAnnotations = currentMethod.getParameterAnnotations();
        int argNum;
        for (argNum = 0; argNum < parameterAnnotations.length; argNum++) {
            for (Annotation a : parameterAnnotations[argNum]) {
                if (a instanceof RequestBody) {
                    return args[argNum];
                }
            }
        }
        return null;
    }

    /**
     * 前置打印
     *
     * @param joinPoint         joinPoint
     * @param request           HttpServletRequest
     * @param methodDescription methodDescription
     * @param requestArgsStr    requestArgsStr
     * @param printArg          printArg
     */
    private void printRequestBeforeLog(JoinPoint joinPoint, HttpServletRequest request, String methodDescription, String requestArgsStr, boolean printArg) {

        // 打印请求相关参数
        log.info("========================================== START ==========================================");
        log.info("[{}] URL : {}", request.getMethod(), request.getRequestURL().toString());
        if (StrUtil.isNotBlank(methodDescription)) {
            log.info("接口描述: {}", methodDescription);
        }
        // 打印调用 controller 的全路径以及执行方法
        log.info("接口类名:{}#{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        log.info("接口IP地址: {}", request.getRemoteAddr());
        if (printArg && StrUtil.isNotBlank(requestArgsStr)) {
            log.info("请求参数:{}", requestArgsStr);
        }
    }

    /**
     * 后置打印 接口耗时和相应数据
     *
     * @param consuming 耗时ms
     * @param resultStr 返回数据
     * @param printArg  打印标识 true表示打印
     */
    private void printRequestAfterLog(long consuming, String resultStr, boolean printArg) {
        log.info("接口耗时 : {} ms", consuming);
        if (printArg && StrUtil.isNotBlank(resultStr)) {
            log.info("接口返回:{}", resultStr);
        }
        log.info("=========================================== END ===========================================");
    }

    /**
     * 方法前打印
     *
     * @param joinPoint         joinPoint
     * @param methodDescription methodDescription
     * @param logArgStr         logArgStr
     * @param printArg          printArg
     */
    private void printMethodBeforeLog(JoinPoint joinPoint, String methodDescription, String logArgStr, boolean printArg) {
        log.info("======= METHOD =======");
        if (StrUtil.isNotBlank(methodDescription)) {
            log.info("方法描述:{}", methodDescription);
        }
        log.info("方法名称: {}#{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        if (printArg && StrUtil.isNotBlank(logArgStr)) {
            log.info("方法参数: {}", logArgStr);
        }

    }

    /**
     * 方法后打印
     *
     * @param consuming 耗时ms
     * @param resultStr 返回数据
     * @param printArg  打印标识
     */
    private void printMethodAfterLog(long consuming, String resultStr, boolean printArg) {
        log.info("方法耗时: {} ms", consuming);
        if (printArg) {
            log.info("方法返回: {}", resultStr);
        }
        log.info("========= END =========");
    }

    /**
     * 获取切面注解的描述
     *
     * @param joinPoint 切点
     * @return 描述信息
     * @throws Exception Exception
     */
    @SneakyThrows
    private String getAspectLogDescription(JoinPoint joinPoint) {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] arguments = joinPoint.getArgs();
        Class<?> targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        StringBuilder description = new StringBuilder("");
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                Class<?>[] clazzs = method.getParameterTypes();
                if (clazzs.length == arguments.length) {
                    description.append(method.getAnnotation(WebLog.class).description());
                    break;
                }
            }
        }
        return description.toString();
    }

    /**
     * webLog的描述信息
     *
     * @param currentMethod Method
     * @param webLog        webLog
     * @return webLog的描述信息
     */
    @SneakyThrows
    private String getAspectLogDescription(Method currentMethod, WebLog webLog) {
        return webLog.description();
    }


}
